/*
 * ModelCC, distributed under ModelCC Shared Software License, www.modelcc.org
 */

package org.modelcc.io.java;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import org.modelcc.*;
import org.modelcc.io.java.checker.*;
import org.modelcc.io.java.processor.*;
import org.modelcc.io.java.visitor.*;
import org.modelcc.language.metamodel.*;
import org.modelcc.lexer.recognizer.PatternRecognizer;
import org.modelcc.metamodel.*;

/**
 * Java model reader
 * 
 * @author Luis Quesada (lquesada@modelcc.org) & Fernando Berzal (fberzal@modelcc.org)
 */
public class JavaLanguageReader extends JavaModelReader<LanguageModel,
                                                        JavaLanguageClass,
                                                        LanguageMember,
                                                        JavaLanguageMetadata> 
{
    /**
     * Constructor
     * @param root the root class
     */
    public JavaLanguageReader(Class root) 
    {
    	super(root);
    }

    /**
     * Create specific instance of JavaModelReaderMetadata
     * @return Reader metadata
     */
    @Override
    public JavaLanguageMetadata createMetadata ()
    {
    	return new JavaLanguageMetadata();
    }
    
    /**
     * Process JavaModelReader metadata
     * @param metadata Metadata obtained through reflection
     * @return The resulting ModelCC model
     * @throws Exception
     */
    @Override
	public LanguageModel createModel(JavaLanguageMetadata metadata) throws Exception
	{
		preProcessModel(metadata);

    	for (JavaLanguageClass java: metadata.getJavaElements()) {
    		LanguageElement element = createElement(metadata, java);
    		metadata.elements.add(element);
    		metadata.javaElementToElement.put( (JavaLanguageClass)java, element);
    		metadata.classToElement.put(element.getElementClass(), element);
    	}
    	
    	postProcessModel(metadata);

    	return new LanguageModel(metadata.elements,metadata.start,metadata.delimiters,metadata.precedences,metadata.subclasses,metadata.superclasses,metadata.classToElement,metadata.defaultElement);
	}
    
	
    // Model preprocessing
    
	private void preProcessModel(JavaLanguageMetadata metadata) 
	{
		// Priorities
		(new PriorityProcessor(this)).process(metadata);
    	
    	// Inherit members.
    	(new MemberInheritanceProcessor(this)).process(metadata);

    	// Recursively manage attribute inheritance
    	(new AttributeInheritanceProcessor(this)).process(metadata);

    	// Convert priority values to precedences.
    	(new PriorityToPrecedenceConverter(this)).process(metadata);

    	// Check precedences for any cycles
    	(new PrecedenceChecker(this)).check(metadata);

    	// Check optional members
    	(new OptionalChecker(this)).check(metadata);

    	// Check references
    	(new ReferenceChecker(this)).check(metadata);
	}
	

	// Model postprocessing
	
	private void postProcessModel(JavaLanguageMetadata metadata) 
	{	
		// Process metadata
		
    	(new PrecedenceProcessor(this)).process(metadata);

    	(new SubclassProcessor(this)).process(metadata);
    	
    	(new SuperclassProcessor(this)).process(metadata);
    	
    	(new PositionProcessor(this)).process(metadata);

    	(new DefaultElementProcessor(this)).process(metadata);

    	// Check for cycles
    	
    	(new CycleChecker(this)).check(metadata);

    	metadata.start = metadata.classToElement.get(root);
	}


	@Override
	protected void preProcessClass (Class type, JavaLanguageMetadata metadata, JavaLanguageClass element)
			throws Exception 
	{
		JavaModelVisitor visitor;
    	
    	// @Value
    	visitor = new ValueVisitor(this);
    	visitor.visitFields(type,element);
    	
    	// @Probability
    	// visitor = new ProbabilityVisitor(this);
    	// visitor.visit(type,element);

    	// @Pattern
    	visitor = new PatternVisitor(this,metadata);
    	visitor.visit(type,element);
    	
    	// @Setup
    	visitor = new SetupVisitor(this);
    	visitor.visitMethods(type,element);

    	// @Constraint
    	visitor = new ConstraintVisitor(this);
    	visitor.visitMethods(type,element);
	}
	

	@Override
	protected void postProcessClass(Class type, JavaLanguageMetadata metadata, JavaLanguageClass element)
			throws Exception 
	{
		JavaModelVisitor visitor;

		// @FreeOrder
    	visitor = new FreeOrderVisitor(this);
    	visitor.visit(type,element);

    	// @Associativity
    	visitor = new AssociativityVisitor(this);
    	visitor.visit(type,element);

    	// @Composition
    	visitor = new CompositionVisitor(this);
    	visitor.visit(type,element);
    	
    	// @Prefix
    	visitor = new PrefixVisitor(this,metadata);
    	visitor.visit(type,element);

    	// @Prefix
    	visitor = new SuffixVisitor(this,metadata);
    	visitor.visit(type,element);

    	// @Separator
    	visitor = new SeparatorVisitor(this,metadata);
    	visitor.visit(type,element);
	}


    @Override
    protected LanguageMember createFieldElement (Field field,Class elementClass,JavaLanguageMetadata metadata) 
    {
    	MemberCollectionType collection = null;
    	Class contentClass;

    	// Collection
    	if (List.class.isAssignableFrom(field.getType())) {
    		collection = MemberCollectionType.LIST;
    	} else if (Set.class.isAssignableFrom(field.getType())) {
    		collection = MemberCollectionType.SET;
    	} else if (Map.class.isAssignableFrom(field.getType())) {
    		return null;
    	} else if (field.getType().isArray()) {
    		collection = MemberCollectionType.ARRAY;
    	}

    	contentClass = Reflection.getType(collection,field);
    	
    	if (!metadata.getRelevantClasses().contains(contentClass)) {
    		if (field.isAnnotationPresent(ID.class) || field.isAnnotationPresent(Multiplicity.class) || field.isAnnotationPresent(Optional.class) || field.isAnnotationPresent(Prefix.class) || field.isAnnotationPresent(Suffix.class) || field.isAnnotationPresent(Separator.class) || field.isAnnotationPresent(Reference.class)) {
    			log(Level.SEVERE, "In field \"{0}\" of class \"{1}\": Field class does not implement IModel but has ModelCC annotations.", new Object[]{field.getName(), elementClass.getCanonicalName()});
    		}
    		return null;
    	}

    
    	LanguageMember member;
    	
    	if (collection == null)
    		member = new LanguageMember(field.getName(),contentClass);
    	else
    		member = new MemberCollection(field.getName(),contentClass,collection);
    	

    	// Annotation visitors
    	
    	JavaModelVisitor visitor;
    	
    	visitor = new OptionalVisitor(this);
    	visitor.visit(field,member);
    	
    	visitor = new IDVisitor(this);
    	visitor.visit(field,member);

    	visitor = new ReferenceVisitor(this);
    	visitor.visit(field,member);

    	visitor = new PrefixVisitor(this,metadata);
    	visitor.visit(field,member);

    	visitor = new SuffixVisitor(this,metadata);
    	visitor.visit(field,member);

    	visitor = new SeparatorVisitor(this,metadata);
    	visitor.visit(field,member);
    	
    	visitor = new MultiplicityVisitor(this);
    	visitor.visit(field,member);

    	// visitor = new ProbabilityVisitor(this);
    	// visitor.visit(field,member);
  	
    	return member;
    }

	/**
	 * Process new element member
	 */
    @Override
	protected void processField (JavaLanguageClass element, LanguageMember lm) 
	{
		if (lm.isKey())
			element.addKeyMember(lm);
	}


    /**
     * Create Java element
     */
    @Override
    protected JavaLanguageClass createClassElement (Class type)
    {
    	return new JavaLanguageClass(type);
    }
 
    
    /**
     * Create ModelCC model element from Java element
     */
	protected LanguageElement createElement (JavaLanguageMetadata metadata, JavaLanguageClass pe) 
	{
		LanguageElement e;
		Class elementClass;
		List<LanguageMember> contents;
		List<LanguageMember> ids;
		boolean freeOrder;
		AssociativityType associativity;
		CompositionType composition;
		List<PatternRecognizer> prefix;
		List<PatternRecognizer> suffix;
		List<PatternRecognizer> separator;
		PatternRecognizer pattern;
		Field valueField;
		Method setupMethod;
		elementClass = pe.getElementClass();
		contents = pe.getMembers();
		ids = pe.getKeyMembers();
		Evaluator evaluator;

		if (pe.isFreeOrder() != null)
			freeOrder = pe.isFreeOrder();
		else
			freeOrder = false;
		if (pe.getAssociativity() == null)
			associativity = AssociativityType.UNDEFINED;
		else
			associativity = pe.getAssociativity();
		if (pe.getComposition() == null)
			composition = CompositionType.UNDEFINED;
		else
			composition = pe.getComposition();

		if (pe.getPrefix()==null)
			prefix = null;
		else
			prefix = pe.getPrefix();
		if (pe.getSuffix()==null)
			suffix = null;
		else
			suffix = pe.getSuffix();
		if (pe.getSeparator()==null)
			separator = null;
		else
			separator = pe.getSeparator();

		pattern = pe.getPattern();
		valueField = pe.getValueField();
		setupMethod = pe.getSetupMethod();
		evaluator = pe.getEvaluator();

		String valueFieldName = null;
		if (valueField != null)
			valueFieldName = valueField.getName();

		String setupMethodName = null;
		if (setupMethod != null)
			setupMethodName = setupMethod.getName();

		List<String> constraintMethodNames = new ArrayList<String>();
		for (int i = 0;i < pe.getConstraintMethods().size();i++) {
			constraintMethodNames.add(pe.getConstraintMethods().get(i).getName());
		}

		e = null;
		if (Modifier.isAbstract(elementClass.getModifiers()) && metadata.getJavaSubclasses(pe) == null) {
			e = new LanguageElement(elementClass,associativity,prefix,suffix,separator,setupMethodName,constraintMethodNames,evaluator);
			log(Level.SEVERE, "In class \"{0}\": Abstract class without subclasses.", new Object[]{elementClass.getCanonicalName()});
		} else if (pe.getMembers().isEmpty() && metadata.getJavaSubclasses(pe) != null) {
			e = new LanguageElement(elementClass,associativity,prefix,suffix,separator,setupMethodName,constraintMethodNames,evaluator);
		} else if (pe.getPattern() != null) {
			if (!Reflection.hasConstructor(elementClass))
				log(Level.SEVERE, "In class \"{0}\": Elements containing @Pattern or @Value need to implement a public parameterless constructor.", new Object[]{elementClass.getCanonicalName()});
			e = new SimpleLanguageElement(elementClass,associativity,prefix,suffix,separator,setupMethodName,constraintMethodNames,pattern,valueFieldName,evaluator);
		} else {
			e = new CompositeLanguageElement(elementClass,associativity,prefix,suffix,separator,setupMethodName,constraintMethodNames,contents,ids,freeOrder,composition,evaluator);
			if (Reflection.hasAnnotation(elementClass,Pattern.class)) {
				contents = new ArrayList<LanguageMember>();
			}
			if (!Reflection.hasConstructor(elementClass))
				log(Level.SEVERE, "In class \"{0}\": Composite elements need to implement a public parameterless constructor.", new Object[]{elementClass.getCanonicalName()});
			if (contents.isEmpty() && !Reflection.hasAnnotation(elementClass,Pattern.class) && metadata.getJavaSubclasses(pe) == null)
				log(Level.SEVERE, "In class \"{0}\": Empty composite element.", new Object[]{elementClass.getCanonicalName()});
		}
		return e;
	}
}
